package org.melanesia.converters;

import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.melanesia.beans.BeanUtils;
import org.melanesia.conf.Config;
import org.melanesia.converters.java.DateToDateConverter;
import org.melanesia.converters.java.NumberToNumberConverter;
import org.melanesia.converters.jdbc.DateToJdbcConverter;
import org.melanesia.converters.jdbc.PassThruJdbcConverter;

/**
 * Container for type converter configuration.
 *
 * Rules:
 *
 * 1. If the value is returned from database, and its type is different than the
 * bean property it is mapped to, there must be a converter.
 *
 * 2. If the value is passed to {@link org.melanesia.sql.Query},
 * {@link org.melanesia.sql.Call} or {@link org.melanesia.sql.Batch}, there must
 * be a converter which tells SQLBuilder what JDBC type to use.
 *
 * 3. Converters are defined for explicit classes. A converter registered for
 * class A, is not applicable to subclasses of A, unless explicitly registered
 * for each of such subclasses.
 *
 * The lack of a converter in above situations will result in an exception being
 * thrown from {@link org.melanesia.sql.Query}/{@link org.melanesia.sql.Call}/
 * {@link org.melanesia.sql.Batch}.
 *
 * @author marcin.kielar
 */
public class TypeConverter {

    /**
     * Internal structure, for holding java to java converters. Objects of this
     * class are used as keys in a map containing java to java converters.
     *
     * Each java to java converter is defined for two explicit classes: -
     * <code>fromClass</code> - a class from which values are converted -
     * <code>toClass</code> - a class to which values are converted.
     *
     * @author marcin.kielar
     *
     */
    private static class ClassPair {

        /** Class from which values are converted. */
        private final Class<?> fromClass;

        /** Class to which values are converted. */
        private final Class<?> toClass;

        /**
         * Constructs a <code>ClassPair</code> object with given pair of
         * classes.
         *
         * @param fromClass
         *            class from which values are converted
         * @param toClass
         *            class to which values are converted
         */
        public ClassPair(final Class<?> fromClass, final Class<?> toClass) {
            this.fromClass = fromClass;
            this.toClass = toClass;
        }

        @Override
        public int hashCode() {
            return (fromClass.getName() + " " + toClass.getName()).hashCode();
        }

        @Override
        public boolean equals(final Object object) {
            if (object instanceof ClassPair) {
                return equals((ClassPair) object);
            }

            return false;
        }

        /**
         * Compares this object, and another <code>ClassPair</code> object.
         *
         * @param classPair
         *            another <code>ClassPair</code> object
         * @return true if objects are equal
         */
        public boolean equals(final ClassPair classPair) {
            return classPair.fromClass.equals(fromClass) && classPair.toClass.equals(toClass);
        }
    }

    /**
     * Collection of converters from java class to JDBC type. This collection is
     * used when passing values to database.
     */
    private final Map<Class<?>, JavaToJdbcConverter> javaToJdbcConverters = new HashMap<Class<?>, JavaToJdbcConverter>();

    /**
     * Collection of java to java converters. This collection is used when
     * mapping result sets to bean's properties.
     */
    private final Map<ClassPair, JavaToJavaConverter> javaToJavaConverters = new HashMap<ClassPair, JavaToJavaConverter>();

    /**
     * Constructs new TypeConverter with initial parameterizations. Initial
     * parameterization consists of registering converters for java simple types
     * (String, Date, numeric and boolean types).
     */
    public TypeConverter() {

        // Java to JDBC converters (pass thru)
        registerConverter(String.class, new PassThruJdbcConverter(Types.VARCHAR));
        registerConverter(java.util.Date.class,
                                            new PassThruJdbcConverter(Types.DATE));

        registerConverter(Long.class, new PassThruJdbcConverter(Types.BIGINT));
        registerConverter(Integer.class, new PassThruJdbcConverter(Types.INTEGER));
        registerConverter(Short.class, new PassThruJdbcConverter(Types.SMALLINT));
        registerConverter(Byte.class, new PassThruJdbcConverter(Types.SMALLINT));
        registerConverter(Float.class, new PassThruJdbcConverter(Types.FLOAT));
        registerConverter(Double.class, new PassThruJdbcConverter(Types.DOUBLE));
        registerConverter(Character.class, new PassThruJdbcConverter(Types.CHAR));
        registerConverter(Boolean.class, new PassThruJdbcConverter(Types.BOOLEAN));

        registerConverter(long.class, new PassThruJdbcConverter(Types.BIGINT));
        registerConverter(int.class, new PassThruJdbcConverter(Types.INTEGER));
        registerConverter(short.class, new PassThruJdbcConverter(Types.SMALLINT));
        registerConverter(byte.class, new PassThruJdbcConverter(Types.SMALLINT));
        registerConverter(float.class, new PassThruJdbcConverter(Types.FLOAT));
        registerConverter(double.class, new PassThruJdbcConverter(Types.DOUBLE));
        registerConverter(char.class, new PassThruJdbcConverter(Types.CHAR));
        registerConverter(boolean.class, new PassThruJdbcConverter(Types.BOOLEAN));

        // Java to JDBC converters (dates)
        registerConverter(java.util.Date.class, new DateToJdbcConverter());
        registerConverter(java.sql.Date.class, new DateToJdbcConverter());
        registerConverter(java.sql.Timestamp.class, new DateToJdbcConverter());
        registerConverter(javax.xml.datatype.XMLGregorianCalendar.class, new DateToJdbcConverter());

        // Number converters
        for (Class<?>[] pair : NumberToNumberConverter.getSupportedConversions()) {
            registerConverter(pair[0], pair[1], new NumberToNumberConverter(pair[1]));
        }

        // Date converters
        for (Class<?>[] pair : DateToDateConverter.getSupportedConversions()) {
            registerConverter(pair[0], pair[1], new DateToDateConverter(pair[1]));
        }

    }

    /**
     * Registers java to jdbc type converter. This collection is used when
     * passing values to database.
     *
     * @param javaClass
     *            java class
     * @param converter
     *            converter
     */
    public final void registerConverter(final Class<?> javaClass, final JavaToJdbcConverter converter) {
        javaToJdbcConverters.put(javaClass, converter);
    }

    /**
     * Registers java to java converter. Those converters are used when mapping
     * result sets to bean's properties.
     *
     * @param fromClass
     *            class from which values are converted
     * @param toClass
     *            class to which values are converted
     * @param converter
     *            converter
     */
    public final void registerConverter(final Class<?> fromClass, final Class<?> toClass, final JavaToJavaConverter converter) {
        javaToJavaConverters.put(new ClassPair(fromClass, toClass), converter);
    }

    /**
     * Searches the repository for a java to jdbc type converter for given
     * class.
     *
     * @param clazz
     *            class to look up the converter
     * @return converter converter
     */
    public final JavaToJdbcConverter findJavaToJdbcConverter(final Class<?> clazz) {

        // 1. Try with classes given
        JavaToJdbcConverter converter = javaToJdbcConverters.get(clazz);

        if (converter == null && Config.isTypeConverterAutoboxingSearch()) {

            // 2. Try with wrapper/primitive class
            if (BeanUtils.isPrimitiveClass(clazz)) {
                converter = javaToJdbcConverters.get(BeanUtils.getWrapperClass(clazz));
            } else if (BeanUtils.isWrapperClass(clazz)) {
                converter = javaToJdbcConverters.get(BeanUtils.getPrimitiveClass(clazz));
            }
        }

        if (converter == null && Config.isTypeConverterSuperClassSearch()) {

            // 3. Try with super class converters
            Class<?> superClass = clazz.getSuperclass();
            while (superClass != null && converter == null) {
                converter = javaToJdbcConverters.get(superClass);
                superClass = superClass.getSuperclass();
            }
        }

        return converter;
    }

    /**
     * Searches the repository for a java to jdbc type converter for given
     * class.
     *
     * @param fromClass
     *            class from which values are converted
     * @param toClass
     *            class to which values are converted
     * @return converter
     */
    public final JavaToJavaConverter findJavaToJavaConverter(final Class<?> fromClass, final Class<?> toClass) {

        // 1. Try with classes given
        JavaToJavaConverter converter = javaToJavaConverters.get(new ClassPair(fromClass, toClass));

        if (converter == null && Config.isTypeConverterAutoboxingSearch()) {

            // 2. Try with wrapper/primitive class

            List<Class<?>> fromClasses = new ArrayList<Class<?>>();
            List<Class<?>> toClasses = new ArrayList<Class<?>>();

            fromClasses.add(fromClass);
            if (BeanUtils.isPrimitiveClass(fromClass)) {
                fromClasses.add(BeanUtils.getWrapperClass(fromClass));
            } else if (BeanUtils.isWrapperClass(fromClass)) {
                fromClasses.add(BeanUtils.getPrimitiveClass(fromClass));
            }

            toClasses.add(toClass);
            if (BeanUtils.isPrimitiveClass(toClass)) {
                toClasses.add(BeanUtils.getWrapperClass(toClass));
            } else if (BeanUtils.isWrapperClass(toClass)) {
                toClasses.add(BeanUtils.getPrimitiveClass(toClass));
            }

            for (int i = 0; i < fromClasses.size() && converter == null; i++) {
                for (int j = 0; j < toClasses.size() && converter == null; j++) {
                    converter = javaToJavaConverters.get(new ClassPair(fromClasses.get(i), toClasses.get(j)));
                }
            }
        }

        if (converter == null && Config.isTypeConverterSuperClassSearch()) {

            // 3. Try with super class converters
            Class<?> fromSuperClass = fromClass.getSuperclass();
            while (fromSuperClass != null && converter == null) {

                converter = javaToJavaConverters.get(new ClassPair(fromSuperClass, toClass));
                if (converter == null && Config.isTypeConverterAutoboxingSearch()) {
                    if (BeanUtils.isPrimitiveClass(toClass)) {
                        converter = javaToJavaConverters.get(new ClassPair(fromSuperClass, BeanUtils.getWrapperClass(toClass)));
                    } else if (BeanUtils.isWrapperClass(toClass)) {
                        converter = javaToJavaConverters.get(new ClassPair(fromSuperClass, BeanUtils.getPrimitiveClass(toClass)));
                    }
                }

                fromSuperClass = fromSuperClass.getSuperclass();
            }
        }

        return converter;
    }
}
